using System;
using System.IO;
using System.Runtime.CompilerServices;
using System.Text;
using Pinwheel.Toolkits;

namespace Pinwheel.LSerialize
{
    public unsafe partial struct LStreamWriter : IDisposable
    {
        static ILFormatter<ValueHeader> ValFormatter;
        static ILFormatter<ReferenceHeader> RefFormatter;
        
        [ThreadStatic] private static LStreamWriter* s_Default;
        public static LStreamWriter* Default
        {
            get
            {
                if (null! == s_Default)
                {
                    s_Default = Native.Current.AllocAndClear<LStreamWriter>();
                    s_Default->buffer = LStreamBuffer.Create();
                    s_Default->assist = LStreamBuffer.Create();
                }
                return s_Default;
            }
        }

        public LStreamWriter* Ptr 
        {
            get
            {
                fixed (LStreamWriter* ptr = &this) return ptr;
            }
        }
        
        private LStreamBuffer* assist;
        private LStreamBuffer* buffer;
        public LStreamBuffer* Buffer
        {
            get
            {
                if (buffer == default)
                {
                    buffer = LStreamBuffer.Create();
                }
                return buffer;
            }
        }

        // private ILStreamBufferWriter writer;

        static LStreamWriter()
        {
            if (null! == s_Default)
            {
                s_Default = Native.Current.AllocAndClear<LStreamWriter>();
                s_Default->buffer = LStreamBuffer.Create();
                s_Default->assist = LStreamBuffer.Create();
            }
            
            ValFormatter = LFormatterProvider.GetFormatter<ValueHeader>();
            RefFormatter = LFormatterProvider.GetFormatter<ReferenceHeader>();
        }

        public LStreamWriter()
        {
            buffer = LStreamBuffer.Create();
            assist = LStreamBuffer.Create(); 
        }
        
        public LStreamWriter(int size)
        {
            buffer = LStreamBuffer.Create(size);
            assist = LStreamBuffer.Create(size);
        }
        
        public void Dispose()
        {
            buffer->Dispose();
            Native.Current.Free(buffer);

            assist->Dispose();
            Native.Current.Free(assist);
        }
 
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public void Reset()
        {
            if (null != buffer) buffer->Clear();
            if (null != assist) assist->Clear();
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public int Seek(int offset, SeekOrigin loc) => Buffer->Seek(offset, loc);

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public byte[] GetBuffer() => Buffer->GetBuffer();
        
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public int GetBuffer(byte* buffer) => Buffer->GetBuffer(buffer);

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public ILFormatter<T> GetFormatter<T>() => LFormatterProvider.GetFormatter<T>();
        
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public ILFormatter GetFormatter(Type type) => LFormatterProvider.GetFormatter(type);

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public int WriteByte(byte value)
        {
            return Buffer->PushByte(value);
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public int WriteUInt16(UInt16 value)
        {
            // int size = 0;
            // size += WriteByte((byte) (value & 0xff));
            // size += WriteByte((byte) ((value >> 8) & 0xff));
            int size = sizeof(UInt16);
            var ptr = &value;
            Buffer->PushBytes((byte*)ptr, size);
            return size;
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public int WriteUInt32(UInt32 value)
        {
            // int size = 0;
            // size += WriteByte((byte) (value & 0xff));
            // size += WriteByte((byte) ((value >> 8) & 0xff));
            // size += WriteByte((byte) ((value >> 16) & 0xff));
            // size += WriteByte((byte) ((value >> 24) & 0xff));
            int size = sizeof(UInt32);
            var ptr = &value;
            Buffer->PushBytes((byte*)ptr, size);
            return size;
        }
        
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public int WriteUInt64(UInt64 value)
        {
            // int size = 0;
            // size += WriteByte((byte) (value & 0xff));
            // size += WriteByte((byte) ((value >> 8) & 0xff));
            // size += WriteByte((byte) ((value >> 16) & 0xff));
            // size += WriteByte((byte) ((value >> 24) & 0xff));
            // size += WriteByte((byte) ((value >> 32) & 0xff));
            // size += WriteByte((byte) ((value >> 40) & 0xff));
            // size += WriteByte((byte) ((value >> 48) & 0xff));
            // size += WriteByte((byte) ((value >> 56) & 0xff));
            // return size;
            int size = sizeof(UInt64);
            var ptr = &value;
            Buffer->PushBytes((byte*)ptr, size);
            return size;
        }
        
        // [MethodImpl(MethodImplOptions.AggressiveInlining)]
        // public int WriteUInt128(UInt128 value)
        // {
        //     int size = 0;
        //     size += WriteByte((byte) (value & 0xff));
        //     size += WriteByte((byte) ((value >> 8) & 0xff));
        //     size += WriteByte((byte) ((value >> 16) & 0xff));
        //     size += WriteByte((byte) ((value >> 24) & 0xff));
        //     size += WriteByte((byte) ((value >> 32) & 0xff));
        //     size += WriteByte((byte) ((value >> 40) & 0xff));
        //     size += WriteByte((byte) ((value >> 48) & 0xff));
        //     size += WriteByte((byte) ((value >> 56) & 0xff));
        //     size += WriteByte((byte) ((value >> 64) & 0xff));
        //     size += WriteByte((byte) ((value >> 72) & 0xff));
        //     size += WriteByte((byte) ((value >> 80) & 0xff));
        //     size += WriteByte((byte) ((value >> 88) & 0xff));
        //     size += WriteByte((byte) ((value >> 96) & 0xff));
        //     size += WriteByte((byte) ((value >> 104) & 0xff));
        //     size += WriteByte((byte) ((value >> 112) & 0xff));
        //     size += WriteByte((byte) ((value >> 120) & 0xff));
        //     return size;
        // }
        
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public int UnsafeWriteUnmanagedValue<T>(ref T value)
        {
            void* ptr = Unsafe.AsPointer<T>(ref value);
            return WriteUnmanagedValue((byte*)ptr, Unsafe.SizeOf<T>());
        }
        
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public int WriteUnmanagedValue<T>(ref T value) where T : unmanaged
        {
            fixed (void* ptr = &value)
            {
                return WriteUnmanagedValue((byte*)ptr, sizeof(T));
            }
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public int WriteUnmanagedValue(byte* ptr, int length) => Buffer->PushBytes(ptr, length);


        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public int WriteUnmanagedArray<T>(ref T[] value) where T : unmanaged
        {
            var size = value.Length * sizeof(T);
            Buffer->EnsureSize(size + sizeof(uint));
            WriteUInt32((uint)value.Length);
            // void* ptr = Unsafe.AsPointer(ref value);
            fixed (void* ptr = value) WriteUnmanagedValue((byte*)ptr, size);
            return size;
        }
        
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public int UnsafeWriteUnmanagedArray<T>(ref T[] value)
        {
            var size = value.Length * Unsafe.SizeOf<T>();
            Buffer->EnsureSize(size + sizeof(uint));
            WriteUInt32((uint)value.Length);
            // void* ptr = Unsafe.AsPointer(ref value);
            fixed (void* ptr = value) WriteUnmanagedValue((byte*) ptr, size); 
            return size;
        }
        
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public int WriteBytes(byte[] value)
        {
            fixed (byte* ptr = &value[0])
            {
                return WriteBytes(ptr, value.Length);
            }
        }
        
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public int WriteBytes(byte* ptr, int length)
        {
            int size = sizeof(uint);
            Buffer->EnsureSize(size);
            WriteUInt32((uint)length);
            return Buffer->PushBytes(ptr, length) + size;
        }
        
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public int WriteString(string value)
        {
            return WriteBytes(Encoding.UTF8.GetBytes(value));
        }
        
        // WriteUniversalField
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public void WriteUniversal<T>(LSerializeContext* context, ref T value)
        {
            var type = typeof(T);
            if (type.IsBuiltin())
            {
                GetFormatter<T>().Serialize(context, Ptr, ref value);  
            }
            else
            {
                context->SetValidFieldCount(1);
                WriteManagedValue<T>(context, ref value);
            }
        }
        
        
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public void Clear() => Buffer->Clear();
        
        [MethodImpl(MethodImplOptions.AggressiveInlining)] 
        public void WriteNumber<T>(LSerializeContext* context, ref T value) where T : unmanaged, IEquatable<T>
        {
            if (!value.Equals(default))
            {
                AssistValHeader(new ValHeader() {Depth = context->Depth, Tag = context->Tag});
                fixed (void* ptr = &value) AssistWriteUnmanaged((ulong*)ptr); 
            }
            context->IncreaseTag();
        }
        
        [MethodImpl(MethodImplOptions.AggressiveInlining)] 
        public void WriteNumber<T>(LSerializeContext* context, T value) where T : unmanaged
        {
            if (!value.Equals(default))
            {
                AssistValHeader(new ValHeader() {Depth = context->Depth, Tag = context->Tag});
                AssistWriteUnmanaged((ulong*)&value);
            }
            context->IncreaseTag();
        }
        
        [MethodImpl(MethodImplOptions.AggressiveInlining)] 
        public void WriteNumber<T>(LSerializeContext* context, ref T? value) where T : unmanaged
        {
            if (value.HasValue)
            {
                AssistValHeader(new ValHeader() {Depth = context->Depth, Tag = context->Tag});
                AssistWriteUnmanaged(value.Value);
            }
            context->IncreaseTag();
        }
        
        [MethodImpl(MethodImplOptions.AggressiveInlining)] 
        public void WriteString(LSerializeContext* context, ref string value)
        {
            if (WriteInstId(context, ref value, out var instId))
            {
                HandleUnmanagedStruct(out RefHeader* header);
                var bytes = Encoding.UTF8.GetBytes(value);
                var length = bytes.Length;
                Buffer->PushBytes(bytes);
                header->InstId = (int)instId;
                header->Size = length;
            } 
        }
        
        // [MethodImpl(MethodImplOptions.AggressiveInlining)]
        // public void Merge(LStreamBuffer* buf)
        // {
        //     var size = buf->Size;
        //     var ptr = buf->Head;
        //     buffer->PushBytes(ptr, size);
        // }
        
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public LStreamBuffer* Assemble()
        {
            var offset = sizeof(int);
            var total = buffer->Size + assist->Size + offset * 2;
            buffer->EnsureSize(total);
            buffer->SignSize(true);
            buffer->Seek(0, SeekOrigin.End);
            var assistSize = assist->Size;
            if (assistSize > 0)
            {
                assist->SignSize(true);
                Native.Utils.Copy(buffer->Current, assist->Current, assist->Size);
            }
            else
            {
                buffer->PushBytes((byte*)&assistSize, offset);
            }
            buffer->Reset(total);
            return buffer;
        }
    }
}